<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <canvas id="myCanvas" width="1080" height="630" style="border: 1px solid;"></canvas>
</body>

</html>
<script>

    const ctx = myCanvas.getContext('2d');
    myCanvas.addEventListener("mousedown", mouseDown);
    myCanvas.addEventListener("mousemove", mouseMove);

    var clickNodes = []
    var GMarkFunc = {}

    function mouseDown(ev) {
        clickNodes.push([ev.clientX, ev.clientY])
    }

    function mouseMove(ev){
        if(clickNodes[clickNodes.length  -1]){
            clickNodes[clickNodes.length  -1] = [ev.clientX, ev.clientY]
        }
    }

    /**
     * (平滑曲线)获得每个途经点的控制点信息, indicatorForcurve是曲线的弯曲程度指标(一般0.45)
     */
    GMarkFunc.getCtrlNodes = function (Nodes, indicatorForcurve) {
        let P1;
        let Ai;
        let Bi;
        let ctrlNodes = [];
        let len = Nodes.length;

        for (let i = 0; i < len; i++) {
            P1 = Nodes[i];
            if (i === 0) {  // 起点时候
                if (len < 3) {
                    Ai = P1;  // 两个点的时候 控制点等于起点
                    Bi = P1;
                } else {  //  3个点和3个点以上
                    let B1 = GMarkFunc.getCNode(Nodes[0], Nodes[1], Nodes[2], indicatorForcurve).Bi;
                    Ai = GMarkFunc.getStartCNode(Nodes[0], Nodes[1], B1);
                    Bi = P1;
                }
            } else if (i === len - 1) {  // 终点时候
                if (len < 3) {
                    Ai = P1;
                    Bi = P1;
                } else { //  3个点和3个点以上
                    let Alen_1 = GMarkFunc.getCNode(Nodes[len - 3], Nodes[len - 2], Nodes[len - 1], indicatorForcurve).Ai;
                    Ai = P1;
                    Bi = GMarkFunc.getEndCNode(Nodes[len - 1], Nodes[len - 2], Alen_1);
                }
            } else {
                let cNode = GMarkFunc.getCNode(Nodes[i - 1], P1, Nodes[i + 1], indicatorForcurve);
                Ai = cNode.Ai;
                Bi = cNode.Bi;
            }

            ctrlNodes.push({
                P1: P1,
                Ai: Ai,
                Bi: Bi
            })
        }

        return ctrlNodes;
    };

    /**
 * (平滑曲线)获得点 P1 处的两个控制点Ai, Bi
 */
    GMarkFunc.getCNode = function (P0, P1, P2, indicatorForCurve) {
        let dvct = GMarkFunc.getTengent(P0, P1, P2),
            dvctAi = dvct.dvctAi,
            dvctBi = dvct.dvctBi,
            nearbyP = GMarkFunc.getNearbyPercent(P0, P1, P2),
            nearbyPAi = nearbyP.PAi,
            nearbyPBi = nearbyP.PBi,
            P0P1 = GMarkFunc.getLenOfTwoPnts(P0, P1),
            P1P2 = GMarkFunc.getLenOfTwoPnts(P1, P2);

        let PAi = nearbyPAi * indicatorForCurve * (P0P1 + P1P2),
            PBi = nearbyPBi * indicatorForCurve * (P1P2 + P0P1),
            Ai = GMarkFunc.getPntInVct(P1, dvctAi, PAi),
            Bi = GMarkFunc.getPntInVct(P1, dvctBi, PBi);
        return {
            Ai: Ai,
            Bi: Bi
        }
    };

    /**
 * (平滑曲线)计算某绘制点附近的平滑切线向量，P点处,  根据向量求切线
 */
    GMarkFunc.getTengent = function ([xA, yA], [xP, yP], [xB, yB]) {
        let lenPB = GMarkFunc.getLenOfTwoPnts([xB, yB], [xP, yP]),
            A1 = GMarkFunc.getPntInVct([xP, yP], [xA - xP, yA - yP], lenPB);  // 1参数是点,2:向量ap,3:bp的长度 根据pb的长度获取他在向量ap上的点[坐标]
        let vctPA1 = [A1[0] - xP, A1[1] - yP],  // 向量
            vctPB = [xB - xP, yB - yP], // 向量
            vctPC = GMarkFunc.vctAdd(vctPA1, vctPB);  // 向量的夹角,平行四边形的对角线
        let vctPM, vctPM1;

        //旋转角小于180是顺时针曲线，超过180是逆时针曲线，两种情况下M、M1点的取值方向是相反的
        let rotateType = GMarkFunc.getRotateType([xA - xP, yA - yP], [xB - xP, yB - yP]);
        if (rotateType === '<=180') {
            vctPM = [-vctPC[1], vctPC[0]];
            vctPM1 = [-vctPM[0], -vctPM[1]];
        } else {
            vctPM = [vctPC[1], -vctPC[0]];
            vctPM1 = [-vctPM[0], -vctPM[1]];
        }

        return {
            dvctAi: vctPM,
            dvctBi: vctPM1
        };
    };


    /**
     * (平滑曲线)计算附近对控制点生成a, b取值的影响比例
     */
    GMarkFunc.getNearbyPercent = function ([xA, yA], [xB, yB], [xC, yC]) {
        //用边长比
        let lenAB = GMarkFunc.getLenOfTwoPnts([xA, yA], [xB, yB]);
        let lenBC = GMarkFunc.getLenOfTwoPnts([xB, yB], [xC, yC]);
        let sidePc = {
            PAi: lenBC / (lenAB + lenBC),    // 分别求两段线所占总长的比例
            PBi: lenAB / (lenAB + lenBC)
        };

        let PAi = Math.pow(sidePc.PAi / 2, 0.6);
        let PBi = Math.pow(sidePc.PBi / 2, 0.6);
        return {
            PAi: PAi,
            PBi: PBi
        };
    };


    /**
 * (平滑曲线)获取起始点的控制点 Ai
 */
    GMarkFunc.getStartCNode = function (A, B, P) {
        if (B[0] === P[0] && B[1] === P[1]) {
            return A;
        }
        let vctBA = GMarkFunc.vct(B, A);
        let vctAB = GMarkFunc.vct(A, B);
        let vctBP = GMarkFunc.vct(B, P);
        let rAngle = GMarkFunc.getRotateAng(vctBA, vctBP);
        rAngle = rAngle > 180 ? rAngle - 360 : rAngle;
        let vctAQ = GMarkFunc.getRotateVct(vctAB, -rAngle * 0.5);
        let Q = GMarkFunc.getPntInVct(A, vctAQ, GMarkFunc.getVctLen(vctBP) * 0.6);
        return Q;
    };

    /**
     * 计算两点距离
     */
    GMarkFunc.getLenOfTwoPnts = function (p1, p2) {
        return Math.sqrt(Math.pow((p1[0] - p2[0]), 2) + Math.pow((p1[1] - p2[1]), 2));
    };

    /**
 * 根据点在向量上的比例计算点坐标, [xO, yO]为起点，[xVct, yVct]为向量，k 为该点在向量方向上的长度
 * 获取
 */
    GMarkFunc.getPntInVct = function ([xO, yO], [xVct, yVct], k) {
        let lenVct = GMarkFunc.getVctLen([xVct, yVct]);  // 获取向量长度
        let stdVct;
        if (lenVct === 0) {
            stdVct = [0, 0];
        } else {
            stdVct = [xVct / lenVct, yVct / lenVct];  // 得到单位向量, 单位向量与原来的向量方向相同
        }
        return [xO + k * stdVct[0], yO + k * stdVct[1]];
    };

    /**
     * 在一段三次贝塞尔曲线上均匀取点, 不包括终点
     * @param counts 一段贝塞尔曲线中取点的数量
     */
    GMarkFunc.getPntsOf3Bezier = function (p0, p1, p2, p3, counts) {
        let per = counts && counts != 0 ? 1 / counts : 0.02;    //取点间隔
        let points = [];
        for (let t = 0; t <= 0.999999; t += per) {
            points.push(GMarkFunc.getPntIn3Bezier(p0, p1, p2, p3, t));
        }
        return points; 
    };

    /**
     * 获取三次贝塞尔曲线上的一点，t∈[0,1]
     * @param t 介于0 ~ 1, 表示点在曲线中的相对位置
     */
    GMarkFunc.getPntIn3Bezier = function (p0, p1, p2, p3, t) {
        let t_ = 1 - t;
        let x = p0[0] * t_ * t_ * t_ + 3 * p1[0] * t * t_ * t_ + 3 * p2[0] * t * t * t_ + p3[0] * t * t * t,
            y = p0[1] * t_ * t_ * t_ + 3 * p1[1] * t * t_ * t_ + 3 * p2[1] * t * t * t_ + p3[1] * t * t * t;
        return [x, y];
    };

    /**
 * 计算向量长度
 */
    GMarkFunc.getVctLen = function ([x, y]) {
        return Math.sqrt(x * x + y * y);
    };

    /**
 * 获取两个向量之间的旋转角类型, 逆时针：0~180还是超过180，用于简化计算
 */
    GMarkFunc.getRotateType = function ([x1, y1], [x2, y2]) {
        let cross = x1 * y2 - x2 * y1;   // 叉积, 法向量
        if (cross < 0) {
            return '>180';
        } else {
            return '<=180';
        }
    };


    /**
     * 创建向量 (从起点到终点的一个向量)
     */
    GMarkFunc.vct = function (start, end) {
        let x = end[0] - start[0];
        let y = end[1] - start[1];
        return [x, y];
    };

    /**
     * 计算两个向量之间的旋转角，逆时针，[0, 2PI]
     */
    GMarkFunc.getRotateAng = function ([x1, y1], [x2, y2]) {
        let EPSILON = 1.0e-8;
        let dist, dot, cross, degree, angle;

        dist = Math.sqrt(x1 * x1 + y1 * y1);
        x1 /= dist;
        y1 /= dist;
        dist = Math.sqrt(x2 * x2 + y2 * y2);
        x2 /= dist;
        y2 /= dist;

        dot = x1 * x2 + y1 * y2;
        if (Math.abs(dot - 1.0) <= EPSILON) {
            angle = 0;
        } else if (Math.abs(dot + 1.0) <= EPSILON) {
            angle = Math.PI;
        } else {
            angle = Math.acos(dot);
            cross = x1 * y2 - x2 * y1;
            if (cross < 0) {
                angle = 2 * Math.PI - angle;
            }
        }
        degree = angle * 180 / Math.PI;
        return degree;
    };

    /**
     * 向量逆时针旋转angle角度后得到新的向量
     */
    GMarkFunc.getRotateVct = function ([x0, y0], angle) {
        let rad = angle * Math.PI / 180;
        let x1 = Math.cos(rad) * x0 - Math.sin(rad) * y0;
        let y1 = Math.sin(rad) * x0 + Math.cos(rad) * y0;
        return [x1, y1];
    };


    /**
     * (平滑曲线)获取终止点的控制点 Bi
     */
    GMarkFunc.getEndCNode = function (C, B, P) {
        if (B[0] === P[0] && B[1] === P[1]) {
            return C;
        }
        let vctBC = GMarkFunc.vct(B, C);
        let vctCB = GMarkFunc.vct(C, B);
        let vctBP = GMarkFunc.vct(B, P);
        let rAngle = GMarkFunc.getRotateAng(vctBC, vctBP);
        rAngle = rAngle > 180 ? rAngle - 360 : rAngle;
        let vctCQ = GMarkFunc.getRotateVct(vctCB, -rAngle * 0.6);
        let Q = GMarkFunc.getPntInVct(C, vctCQ, GMarkFunc.getVctLen(vctBP) * 0.7);
        return Q;
    };


    /**
 * 向量加法: 几何意义是两个两个向量的中间值,长度是原来的两者长度之和
 */
    GMarkFunc.vctAdd = function (vct1, vct2) {
        return [vct1[0] + vct2[0], vct1[1] + vct2[1]];
    };

    setInterval(() => {
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        var curveNodes = [];    //整条曲线的点
        var cNodes = GMarkFunc.getCtrlNodes(clickNodes, 0.5);
        for (let i = 0; i < clickNodes.length - 1; i++) {
            curveNodes = curveNodes.concat(GMarkFunc.getPntsOf3Bezier(clickNodes[i], cNodes[i].Ai, cNodes[i + 1].Bi, clickNodes[i + 1], 20));
        }
        if (curveNodes && curveNodes.length > 0) {
            ctx.beginPath();
            for (let index = 0; index < curveNodes.length; index++) {
                const p = curveNodes[index];
                if (index === 0) {
                    ctx.moveTo(p[0], p[1]);
                } else {
                    ctx.lineTo(p[0], p[1]);
                }
            }
            ctx.stroke();
        }
    }, 0);


</script>
<style>
    canvas {
        background-image: linear-gradient(45deg, rgba(0, 0, 0, .10) 25%, transparent 0, transparent 75%, rgba(0, 0, 0, .10) 0),
            linear-gradient(45deg, rgba(0, 0, 0, .10) 25%, transparent 0, transparent 75%, rgba(0, 0, 0, .10) 0);
        background-color: #eee;
        background-size: 30px 30px;
        background-position: 0 0, 15px 15px;
    }
</style>